/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule Text
 * @flow
 */
'use strict';

const ColorPropType = require('ColorPropType');
const EdgeInsetsPropType = require('EdgeInsetsPropType');
const NativeMethodsMixin = require('NativeMethodsMixin');
const Platform = require('Platform');
const React = require('React');
const PropTypes = require('prop-types');
const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
const StyleSheetPropType = require('StyleSheetPropType');
const TextStylePropTypes = require('TextStylePropTypes');
const Touchable = require('Touchable');

const createReactClass = require('create-react-class');
const createReactNativeComponentClass = require('createReactNativeComponentClass');
const mergeFast = require('mergeFast');
const processColor = require('processColor');

const stylePropType = StyleSheetPropType(TextStylePropTypes);

const viewConfig = {
  validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
    isHighlighted: true,
    numberOfLines: true,
    ellipsizeMode: true,
    allowFontScaling: true,
    disabled: true,
    selectable: true,
    selectionColor: true,
    adjustsFontSizeToFit: true,
    minimumFontScale: true,
    textBreakStrategy: true,
  }),
  uiViewClassName: 'RCTText',
};

/**
 * A React component for displaying text.
 *
 * `Text` supports nesting, styling, and touch handling.
 *
 * In the following example, the nested title and body text will inherit the
 * `fontFamily` from `styles.baseText`, but the title provides its own
 * additional styles.  The title and body will stack on top of each other on
 * account of the literal newlines:
 *
 * ```ReactNativeWebPlayer
 * import React, { Component } from 'react';
 * import { AppRegistry, Text, StyleSheet } from 'react-native';
 *
 * export default class TextInANest extends Component {
 *   constructor(props) {
 *     super(props);
 *     this.state = {
 *       titleText: "Bird's Nest",
 *       bodyText: 'This is not really a bird nest.'
 *     };
 *   }
 *
 *   render() {
 *     return (
 *       <Text style={styles.baseText}>
 *         <Text style={styles.titleText} onPress={this.onPressTitle}>
 *           {this.state.titleText}{'\n'}{'\n'}
 *         </Text>
 *         <Text numberOfLines={5}>
 *           {this.state.bodyText}
 *         </Text>
 *       </Text>
 *     );
 *   }
 * }
 *
 * const styles = StyleSheet.create({
 *   baseText: {
 *     fontFamily: 'Cochin',
 *   },
 *   titleText: {
 *     fontSize: 20,
 *     fontWeight: 'bold',
 *   },
 * });
 *
 * // skip this line if using Create React Native App
 * AppRegistry.registerComponent('TextInANest', () => TextInANest);
 * ```
 *
 * ## Nested text
 *
 * Both iOS and Android allow you to display formatted text by annotating
 * ranges of a string with specific formatting like bold or colored text
 * (`NSAttributedString` on iOS, `SpannableString` on Android). In practice,
 * this is very tedious. For React Native, we decided to use web paradigm for
 * this where you can nest text to achieve the same effect.
 *
 *
 * ```ReactNativeWebPlayer
 * import React, { Component } from 'react';
 * import { AppRegistry, Text } from 'react-native';
 *
 * export default class BoldAndBeautiful extends Component {
 *   render() {
 *     return (
 *       <Text style={{fontWeight: 'bold'}}>
 *         I am bold
 *         <Text style={{color: 'red'}}>
 *           and red
 *         </Text>
 *       </Text>
 *     );
 *   }
 * }
 *
 * // skip this line if using Create React Native App
 * AppRegistry.registerComponent('AwesomeProject', () => BoldAndBeautiful);
 * ```
 *
 * Behind the scenes, React Native converts this to a flat `NSAttributedString`
 * or `SpannableString` that contains the following information:
 *
 * ```javascript
 * "I am bold and red"
 * 0-9: bold
 * 9-17: bold, red
 * ```
 *
 * ## Nested views (iOS only)
 *
 * On iOS, you can nest views within your Text component. Here's an example:
 *
 * ```ReactNativeWebPlayer
 * import React, { Component } from 'react';
 * import { AppRegistry, Text, View } from 'react-native';
 *
 * export default class BlueIsCool extends Component {
 *   render() {
 *     return (
 *       <Text>
 *         There is a blue square
 *         <View style={{width: 50, height: 50, backgroundColor: 'steelblue'}} />
 *         in between my text.
 *       </Text>
 *     );
 *   }
 * }
 *
 * // skip this line if using Create React Native App
 * AppRegistry.registerComponent('AwesomeProject', () => BlueIsCool);
 * ```
 *
 * > In order to use this feature, you must give the view a `width` and a `height`.
 *
 * ## Containers
 *
 * The `<Text>` element is special relative to layout: everything inside is no
 * longer using the flexbox layout but using text layout. This means that
 * elements inside of a `<Text>` are no longer rectangles, but wrap when they
 * see the end of the line.
 *
 * ```javascript
 * <Text>
 *   <Text>First part and </Text>
 *   <Text>second part</Text>
 * </Text>
 * // Text container: all the text flows as if it was one
 * // |First part |
 * // |and second |
 * // |part       |
 *
 * <View>
 *   <Text>First part and </Text>
 *   <Text>second part</Text>
 * </View>
 * // View container: each text is its own block
 * // |First part |
 * // |and        |
 * // |second part|
 * ```
 *
 * ## Limited Style Inheritance
 *
 * On the web, the usual way to set a font family and size for the entire
 * document is to take advantage of inherited CSS properties like so:
 *
 * ```css
 * html {
 *   font-family: 'lucida grande', tahoma, verdana, arial, sans-serif;
 *   font-size: 11px;
 *   color: #141823;
 * }
 * ```
 *
 * All elements in the document will inherit this font unless they or one of
 * their parents specifies a new rule.
 *
 * In React Native, we are more strict about it: **you must wrap all the text
 * nodes inside of a `<Text>` component**. You cannot have a text node directly
 * under a `<View>`.
 *
 *
 * ```javascript
 * // BAD: will raise exception, can't have a text node as child of a <View>
 * <View>
 *   Some text
 * </View>
 *
 * // GOOD
 * <View>
 *   <Text>
 *     Some text
 *   </Text>
 * </View>
 * ```
 *
 * You also lose the ability to set up a default font for an entire subtree.
 * The recommended way to use consistent fonts and sizes across your
 * application is to create a component `MyAppText` that includes them and use
 * this component across your app. You can also use this component to make more
 * specific components like `MyAppHeaderText` for other kinds of text.
 *
 * ```javascript
 * <View>
 *   <MyAppText>Text styled with the default font for the entire application</MyAppText>
 *   <MyAppHeaderText>Text styled as a header</MyAppHeaderText>
 * </View>
 * ```
 *
 * Assuming that `MyAppText` is a component that simply renders out its
 * children into a `Text` component with styling, then `MyAppHeaderText` can be
 * defined as follows:
 *
 * ```javascript
 * class MyAppHeaderText extends Component {
 *   render() {
 *     <MyAppText>
 *       <Text style={{fontSize: 20}}>
 *         {this.props.children}
 *       </Text>
 *     </MyAppText>
 *   }
 * }
 * ```
 *
 * Composing `MyAppText` in this way ensures that we get the styles from a
 * top-level component, but leaves us the ability to add / override them in
 * specific use cases.
 *
 * React Native still has the concept of style inheritance, but limited to text
 * subtrees. In this case, the second part will be both bold and red.
 *
 * ```javascript
 * <Text style={{fontWeight: 'bold'}}>
 *   I am bold
 *   <Text style={{color: 'red'}}>
 *     and red
 *   </Text>
 * </Text>
 * ```
 *
 * We believe that this more constrained way to style text will yield better
 * apps:
 *
 * - (Developer) React components are designed with strong isolation in mind:
 * You should be able to drop a component anywhere in your application,
 * trusting that as long as the props are the same, it will look and behave the
 * same way. Text properties that could inherit from outside of the props would
 * break this isolation.
 *
 * - (Implementor) The implementation of React Native is also simplified. We do
 * not need to have a `fontFamily` field on every single element, and we do not
 * need to potentially traverse the tree up to the root every time we display a
 * text node. The style inheritance is only encoded inside of the native Text
 * component and doesn't leak to other components or the system itself.
 *
 */

const Text = createReactClass({
  displayName: 'Text',
  propTypes: {
    /**
     * When `numberOfLines` is set, this prop defines how text will be truncated.
     * `numberOfLines` must be set in conjunction with this prop.
     *
     * This can be one of the following values:
     *
     * - `head` - The line is displayed so that the end fits in the container and the missing text
     * at the beginning of the line is indicated by an ellipsis glyph. e.g., "...wxyz"
     * - `middle` - The line is displayed so that the beginning and end fit in the container and the
     * missing text in the middle is indicated by an ellipsis glyph. "ab...yz"
     * - `tail` - The line is displayed so that the beginning fits in the container and the
     * missing text at the end of the line is indicated by an ellipsis glyph. e.g., "abcd..."
     * - `clip` - Lines are not drawn past the edge of the text container.
     *
     * The default is `tail`.
     *
     * > `clip` is working only for iOS
     */
    ellipsizeMode: PropTypes.oneOf(['head', 'middle', 'tail', 'clip']),
    /**
     * Used to truncate the text with an ellipsis after computing the text
     * layout, including line wrapping, such that the total number of lines
     * does not exceed this number.
     *
     * This prop is commonly used with `ellipsizeMode`.
     */
    numberOfLines: PropTypes.number,
    /**
     * Set text break strategy on Android API Level 23+, possible values are `simple`, `highQuality`, `balanced`
     * The default value is `highQuality`.
     * @platform android
     */
    textBreakStrategy: PropTypes.oneOf(['simple', 'highQuality', 'balanced']),
    /**
     * Invoked on mount and layout changes with
     *
     *   `{nativeEvent: {layout: {x, y, width, height}}}`
     */
    onLayout: PropTypes.func,
    /**
     * This function is called on press.
     *
     * e.g., `onPress={() => console.log('1st')}`
     */
    onPress: PropTypes.func,
    /**
     * This function is called on long press.
     *
     * e.g., `onLongPress={this.increaseSize}>`
     */
    onLongPress: PropTypes.func,
    /**
     * When the scroll view is disabled, this defines how far your touch may
     * move off of the button, before deactivating the button. Once deactivated,
     * try moving it back and you'll see that the button is once again
     * reactivated! Move it back and forth several times while the scroll view
     * is disabled. Ensure you pass in a constant to reduce memory allocations.
     */
    pressRetentionOffset: EdgeInsetsPropType,
    /**
     * Lets the user select text, to use the native copy and paste functionality.
     */
    selectable: PropTypes.bool,
    /**
     * The highlight color of the text.
     * @platform android
     */
    selectionColor: ColorPropType,
    /**
     * When `true`, no visual change is made when text is pressed down. By
     * default, a gray oval highlights the text on press down.
     * @platform ios
     */
    suppressHighlighting: PropTypes.bool,
    style: stylePropType,
    /**
     * Used to locate this view in end-to-end tests.
     */
    testID: PropTypes.string,
    /**
     * Used to locate this view from native code.
     */
    nativeID: PropTypes.string,
    /**
     * Specifies whether fonts should scale to respect Text Size accessibility settings. The
     * default is `true`.
     */
    allowFontScaling: PropTypes.bool,
    /**
     * When set to `true`, indicates that the view is an accessibility element. The default value
     * for a `Text` element is `true`.
     *
     * See the
     * [Accessibility guide](docs/accessibility.html#accessible-ios-android)
     * for more information.
     */
    accessible: PropTypes.bool,
    /**
     * Specifies whether font should be scaled down automatically to fit given style constraints.
     * @platform ios
     */
    adjustsFontSizeToFit: PropTypes.bool,

    /**
     * Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
     * @platform ios
     */
    minimumFontScale: PropTypes.number,
    /**
     * Specifies the disabled state of the text view for testing purposes
     * @platform android
     */
    disabled: PropTypes.bool,
  },
  getDefaultProps(): Object {
    return {
      accessible: true,
      allowFontScaling: true,
      ellipsizeMode: 'tail',
    };
  },
  getInitialState: function(): Object {
    return mergeFast(Touchable.Mixin.touchableGetInitialState(), {
      isHighlighted: false,
    });
  },
  mixins: [NativeMethodsMixin],
  viewConfig: viewConfig,
  getChildContext(): Object {
    return {isInAParentText: true};
  },
  childContextTypes: {
    isInAParentText: PropTypes.bool
  },
  contextTypes: {
    isInAParentText: PropTypes.bool
  },
  /**
   * Only assigned if touch is needed.
   */
  _handlers: (null: ?Object),
  _hasPressHandler(): boolean {
    return !!this.props.onPress || !!this.props.onLongPress;
  },
  /**
   * These are assigned lazily the first time the responder is set to make plain
   * text nodes as cheap as possible.
   */
  touchableHandleActivePressIn: (null: ?Function),
  touchableHandleActivePressOut: (null: ?Function),
  touchableHandlePress: (null: ?Function),
  touchableHandleLongPress: (null: ?Function),
  touchableGetPressRectOffset: (null: ?Function),
  render(): React.Element<any> {
    let newProps = this.props;
    if (this.props.onStartShouldSetResponder || this._hasPressHandler()) {
      if (!this._handlers) {
        this._handlers = {
          onStartShouldSetResponder: (): bool => {
            const shouldSetFromProps = this.props.onStartShouldSetResponder &&
                // $FlowFixMe(>=0.41.0)
                this.props.onStartShouldSetResponder();
            const setResponder = shouldSetFromProps || this._hasPressHandler();
            if (setResponder && !this.touchableHandleActivePressIn) {
              // Attach and bind all the other handlers only the first time a touch
              // actually happens.
              for (const key in Touchable.Mixin) {
                if (typeof Touchable.Mixin[key] === 'function') {
                  (this: any)[key] = Touchable.Mixin[key].bind(this);
                }
              }
              this.touchableHandleActivePressIn = () => {
                if (this.props.suppressHighlighting || !this._hasPressHandler()) {
                  return;
                }
                this.setState({
                  isHighlighted: true,
                });
              };

              this.touchableHandleActivePressOut = () => {
                if (this.props.suppressHighlighting || !this._hasPressHandler()) {
                  return;
                }
                this.setState({
                  isHighlighted: false,
                });
              };

              this.touchableHandlePress = (e: SyntheticEvent<>) => {
                this.props.onPress && this.props.onPress(e);
              };

              this.touchableHandleLongPress = (e: SyntheticEvent<>) => {
                this.props.onLongPress && this.props.onLongPress(e);
              };

              this.touchableGetPressRectOffset = function(): RectOffset {
                return this.props.pressRetentionOffset || PRESS_RECT_OFFSET;
              };
            }
            return setResponder;
          },
          onResponderGrant: function(e: SyntheticEvent<>, dispatchID: string) {
            this.touchableHandleResponderGrant(e, dispatchID);
            this.props.onResponderGrant &&
              this.props.onResponderGrant.apply(this, arguments);
          }.bind(this),
          onResponderMove: function(e: SyntheticEvent<>) {
            this.touchableHandleResponderMove(e);
            this.props.onResponderMove &&
              this.props.onResponderMove.apply(this, arguments);
          }.bind(this),
          onResponderRelease: function(e: SyntheticEvent<>) {
            this.touchableHandleResponderRelease(e);
            this.props.onResponderRelease &&
              this.props.onResponderRelease.apply(this, arguments);
          }.bind(this),
          onResponderTerminate: function(e: SyntheticEvent<>) {
            this.touchableHandleResponderTerminate(e);
            this.props.onResponderTerminate &&
              this.props.onResponderTerminate.apply(this, arguments);
          }.bind(this),
          onResponderTerminationRequest: function(): bool {
            // Allow touchable or props.onResponderTerminationRequest to deny
            // the request
            var allowTermination = this.touchableHandleResponderTerminationRequest();
            if (allowTermination && this.props.onResponderTerminationRequest) {
              allowTermination = this.props.onResponderTerminationRequest.apply(this, arguments);
            }
            return allowTermination;
          }.bind(this),
        };
      }
      newProps = {
        ...this.props,
        ...this._handlers,
        isHighlighted: this.state.isHighlighted,
      };
    }
    if (newProps.selectionColor != null) {
      newProps = {
        ...newProps,
        selectionColor: processColor(newProps.selectionColor)
      };
    }
    if (Touchable.TOUCH_TARGET_DEBUG && newProps.onPress) {
      newProps = {
        ...newProps,
        style: [this.props.style, {color: 'magenta'}],
      };
    }
    if (this.context.isInAParentText) {
      return <RCTVirtualText {...newProps} />;
    } else {
      return <RCTText {...newProps} />;
    }
  },
});

type RectOffset = {
  top: number,
  left: number,
  right: number,
  bottom: number,
}

var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};

var RCTText = createReactNativeComponentClass(
  viewConfig.uiViewClassName,
  () => viewConfig
);
var RCTVirtualText = RCTText;

if (Platform.OS === 'android') {
  RCTVirtualText = createReactNativeComponentClass('RCTVirtualText', () => ({
    validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
      isHighlighted: true,
    }),
    uiViewClassName: 'RCTVirtualText',
  }));
}

module.exports = Text;
